package test.threads.queue.common;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.*;

import org.apache.commons.cli.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import test.helpers.Config;
import test.threads.ThreadAffinity;
import test.threads.ThreadAffinity.Core;
import test.threads.queue.common.ITask.BenchmarkResult;
import test.threads.queue.stressed.cpu.CPUWasterFactory;
import test.threads.queue.stressed.memory.CPUMemoryWasterFactory;
import test.threads.queue.stressed.memory.MemoryWasterFactory;
import test.threads.queue.unstressed.BenchmarkThreadFactory;
import test.threads.queue.unstressed.DummyWasterFactory;

/**
 * @author cheremin
 * @since 29.02.12,  14:09
 */
public class TaskBenchmark {
    private static final Log log = LogFactory.getLog( TaskBenchmark.class );

    public static final int QUEUE_CAPACITY = 32 << 10;//32K

    public static final int EVENTS_IN_BATCH = 512;

    private static final int TURNS = 5;

    private static final long DEFAULT_BENCHMARK_DURATION_SECONDS = 30;//=30 sec
    private static final long DEFAULT_WARMUP_DURATION_SECONDS = 30;//=30 sec

    private static final String DEFAULT_CONFIG_FILE = "./config.properties";


    /** Arguments: (taskClass: "...") (usefulWorkFactoryID: "cpu"/"memory"/...) [taskSize: long] */
    public static void main( final String[] args ) throws Exception {
        System.out.printf( "Java: %s, %s (%s)\n",
                           System.getProperty( "java.version" ),
                           System.getProperty( "java.vendor" ),
                           System.getProperty( "java.home" )
        );
        System.out.printf( "Runtime: %s, VM: %s\n",
                           System.getProperty( "java.runtime.version" ),
                           System.getProperty( "java.vm.version" )
        );
        System.out.printf( "OS: %s %s (%s)\n\n",
                           System.getProperty( "os.name" ),
                           System.getProperty( "os.version" ),
                           System.getProperty( "os.arch" )
        );

        final Options options = new Options();
        options.addOption( OptionBuilder.withLongOpt( "task" )
                                   .withDescription( "class of task to benchmark" )
                                   .hasArg()
                                   .isRequired()
                                   .withArgName( "TASK" )
                                   .create()
        );
        options.addOption( OptionBuilder.withLongOpt( "payload" )
                                   .withDescription( "type of payload for benchmark" )
                                   .hasArg()
                                   .isRequired()
                                   .withArgName( "payload" )
                                   .create()
        );

        options.addOption( OptionBuilder.withLongOpt( "config" )
                                   .withDescription( ".properties file containing configuration data (default is " + DEFAULT_CONFIG_FILE + ")" )
                                   .hasArg()
                                   .withArgName( "CONFIG" )
                                   .create()
        );
        options.addOption( OptionBuilder.withArgName( "property=value" )
                                   .hasArgs( 2 )
                                   .withValueSeparator()
                                   .withDescription( "use value for given property" )
                                   .create( "D" )
        );

        final CommandLineParser parser = new GnuParser();
        try {
            final CommandLine cmd = parser.parse( options, args );


            final String taskClass = cmd.getOptionValue( "task" );
            final String usefulWorkFactoryID = cmd.getOptionValue( "payload" );

//            final int opsPerPacket = Integer.parseInt( cmd.getOptionValue( "opsPerPacket" ) );

            final String configFile;
            if ( cmd.hasOption( "config" ) ) {
                configFile = cmd.getOptionValue( "config" );
            } else {
                configFile = DEFAULT_CONFIG_FILE;
            }
            final Config config = Config.load( new File( configFile ) );

            final Properties properties = cmd.getOptionProperties( "D" );
            config.append( properties );

            config.print( System.out );

            final ThreadFactory threadFactory = createThreadFactory( config );

            //create task
            final ITask task = createTaskInstance( taskClass, config );

            final int workersCount = task.workersRequired();
            //create pool
            final ExecutorService pool = Executors.newFixedThreadPool( workersCount, threadFactory );

            //create stress factory
            final IUsefulWorkFactory usefulWorkFactory = createUsefulWorkFactory(
                    usefulWorkFactoryID,
                    config
            );

//            final int opsPerNode = opsPerPacket / workersCount;
            final IUsefulWorkEmulator[] emulators = usefulWorkFactory.createEmulators( workersCount );

            task.initialize( pool, emulators, usefulWorkFactory );


            final String taskName = task.toString();
            final String payloadID = usefulWorkFactory.toString();

            final long benchmarkDurationMs = TimeUnit.SECONDS.toMillis(
                    config.getAsLong(
                            "general.benchmark-duration",
                            DEFAULT_BENCHMARK_DURATION_SECONDS
                    )
            );
            final long warmUpDurationMs = TimeUnit.SECONDS.toMillis(
                    config.getAsLong(
                            "general.warm-up-duration",
                            DEFAULT_WARMUP_DURATION_SECONDS
                    )
            );
            final int turns = config.getAsInt( "general.turns", TURNS );
            final int measurements = config.getAsInt( "general.measurements", 1 );
            System.out.printf(
                    "%s: %d s for warm up, %d for benchmark, payload: %s, %d turns of %d measurements\n\n",
                    task,
                    warmUpDurationMs / 1000,
                    benchmarkDurationMs / 1000,
                    usefulWorkFactory,
                    turns,
                    measurements
            );
            log.info( "\n\n\n" );
            try {
                {//warm up JIT
                    final BenchmarkResult[] results = task.execute( warmUpDurationMs, warmUpDurationMs, 1 );
                    for ( final BenchmarkResult result : results ) {
                        System.out.println( "Warm up " + result.print( taskName, payloadID ) );
                    }
                    System.out.println();
                }
                for ( int i = 0; i < turns; i++ ) {
                    final BenchmarkResult[] results = task.execute( warmUpDurationMs, benchmarkDurationMs, measurements );
                    for ( final BenchmarkResult result : results ) {
                        System.out.println( result.print( taskName, payloadID ) );
                        log.info( result.printTabular( taskName, payloadID ) );
                    }
                    System.out.println();
                    log.info( "" );

                    System.gc();
                    System.gc();
                    Thread.sleep( 500 );//give GC time to finish async tasks
                }
            } catch ( Exception e ) {
                e.printStackTrace( System.err );
            } finally {
                task.terminate();

                pool.shutdown();
                pool.awaitTermination( 1, TimeUnit.SECONDS );
                pool.shutdownNow();
            }
            System.exit( 0 );
        } catch ( org.apache.commons.cli.ParseException e ) {
            log.error( "Error parsing command line arguments", e );
        }
        final HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp( "vanga", options );
    }

    private static ITask createTaskInstance( final String taskClass,
                                             final Config config ) throws InstantiationException, IllegalAccessException, ClassNotFoundException, InvocationTargetException {
        final Class<? extends ITask> clazz = ( Class<? extends ITask> ) Class.forName( taskClass );
        try {
            final Constructor<? extends ITask> ctor = clazz.getConstructor( Config.class );
            return ctor.newInstance( config );
        } catch ( NoSuchMethodException e ) {
            return clazz.newInstance();
        }
    }

    private static IUsefulWorkFactory<?> createUsefulWorkFactory( final String usefulWorkFactoryID,
                                                                  final Config config ) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        if ( "cpu".equals( usefulWorkFactoryID ) ) {
            return new CPUWasterFactory( config );
        } else if ( "memory".equals( usefulWorkFactoryID ) ) {
            return new MemoryWasterFactory( config );
        } else if ( "cpu-memory".equals( usefulWorkFactoryID ) ) {
            return new CPUMemoryWasterFactory( config );
        } else if ( "none".equals( usefulWorkFactoryID ) ) {
            return new DummyWasterFactory( config );
        }
        throw new UnsupportedOperationException( "Method is not implemented for " + usefulWorkFactoryID );
    }

    private static ThreadFactory createThreadFactory( final Config config ) throws ParseException {
        if ( config.hasProperty( "general.affinity" ) ) {
            final String affinityStr = config.getAsString( "general.affinity", null );
            try {
                final Core[] cores = ThreadAffinity.parseCores( affinityStr, null );
                if ( cores != null && cores.length > 1 ) {
                    System.out.printf( "Cores to attach: %s \n", Arrays.toString( cores ) );
                    //set thread priority
                    final Core mainThreadCore = cores[0];

                    BenchmarkThreadFactory.setupCurrentThreadAsBenchmarking( mainThreadCore );

                    final BenchmarkThreadFactory factory = new BenchmarkThreadFactory( "benchmarking-thread", cores );

                    //just to move over first core, which is already used for main thread
                    factory.newThread( null );

                    return factory;
                }
            } catch ( ParseException e ) {
                System.err.printf(
                        "Affinity string [%s] parsing error: %s\n",
                        affinityStr,
                        e.getMessage()
                );
            }
        }
        return new BenchmarkThreadFactory( "benchmarking-thread" );

        //set process priority
        /*try {
            ThreadAffinity.nice( -20 );
        } catch ( Exception e ) {
            System.out.printf( "Can't set nice(-20) -> %s\n", e.getMessage() );
        }*/

    }
}